package debtbundler.presentation.balancesheeteditor;

import java.awt.Color;
import java.awt.Component;
import java.awt.Dimension;
import java.awt.event.FocusEvent;
import java.awt.event.FocusListener;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedHashSet;
import java.util.Map;
import java.util.Set;
import java.util.logging.Level;
import java.util.logging.Logger;

import javax.swing.BorderFactory;
import javax.swing.JComponent;
import javax.swing.JPanel;

import debtbundler.domain.bll.CurrentData;
import debtbundler.domain.bll.PersonSharedCostPair;
import debtbundler.domain.bll.SharingMethod;
import debtbundler.domain.entity.BalanceSheet;
import debtbundler.domain.entity.Person;
import debtbundler.domain.entity.SharedCost;
import debtbundler.presentation.VAlignment;
import debtbundler.presentation.basecomponents.DomainEditorComponent;
import debtbundler.presentation.basecomponents.DomainEditorImpl;
import debtbundler.presentation.basecomponents.DomainEditorListener;
import debtbundler.presentation.basecomponents.DomainViewComponent;
import debtbundler.presentation.basecomponents.SelectableComponent;
import debtbundler.presentation.layout.MultiSizeGridLayout;

/**
 * A matrix-like editor for persons, shared costs, payments and shares
 * in a given balance sheet.
 * <p>
 * Informs <code>GridSelectionListener</code>s when components in grid 
 * are selected or deselected. Does not provide functionality to add
 * or remove persons or costs.
 * 
 * @author Anssi
 *
 */
public class BalanceSheetGrid extends DomainEditorImpl<BalanceSheet> implements DomainEditorListener {
	private static final long serialVersionUID = 1L;
	private static Logger logger = Logger.getLogger(BalanceSheetGrid.class.getName());
	
	public static enum Orientation {PERSONS_ON_COLS, PERSONS_ON_ROWS}
	
	protected CurrentData currentData;
	protected BalanceSheet sheet;
	
	protected SelectableComponentListener selectableComponentListener = new SelectableComponentListener();
	
	protected Map<PersonSharedCostPair, TransactionEditor> transactionEditors = new HashMap<PersonSharedCostPair, TransactionEditor>();
	protected Map<Person, PersonEditor> personEditors = new HashMap<Person, PersonEditor>();
	protected Map<SharedCost, SharedCostEditor> sharedCostEditors = new HashMap<SharedCost, SharedCostEditor>();
	protected Map<Person, PersonBalancePanel> personBalances = new HashMap<Person, PersonBalancePanel>();
	protected Map<SharedCost, SharedCostBalancePanel> sharedCostBalances = new HashMap<SharedCost, SharedCostBalancePanel>();
	protected SelectableComponent selectedComponent = null;
	
	protected Set<DomainEditorListener> domainEditorListeners = new HashSet<DomainEditorListener>(); 
	protected Set<GridSelectionListener> gridListeners = new HashSet<GridSelectionListener>();
	
	protected Orientation orientation = Orientation.PERSONS_ON_COLS;

	/**
	 * Constructs a new balance sheet grid for the given sheet.
	 * 
	 * @param sheet	the sheet to create a grid for
	 */
	public BalanceSheetGrid(CurrentData currentData) {
		this.currentData = currentData;
		this.sheet = currentData.getCurrentSheet();
		syncWithBalanceSheet();
	}
	
	/**
	 * Synchronizes the GUI with the state of the balance sheet it represents.
	 */
	protected void syncWithBalanceSheet() {		
		logger.fine("Synchronizing grid with balance sheet");
		
		Set<Person> personsInSheet = new HashSet<Person>(sheet.getPersons());
		Set<Person> personsInGrid = new HashSet<Person>(personEditors.keySet());
		for (Person person : personsInSheet) {
			if (!personsInGrid.contains(person)) {
				addEditorsFor(person);
			}
		}
		for (Person person : personsInGrid) {
			if (!personsInSheet.contains(person)) {
				removeEditorsFor(person);
			}
		}
		
		Set<SharedCost> costsInSheet = new HashSet<SharedCost>(sheet.getSharedCosts());
		Set<SharedCost> costsInGrid = new HashSet<SharedCost>(sharedCostEditors.keySet());
		for (SharedCost cost : costsInSheet) {
			if (!costsInGrid.contains(cost)) {
				addEditorsFor(cost);
			}
		}
		for (SharedCost cost : costsInGrid) {
			if (!costsInSheet.contains(cost)) {
				removeEditorsFor(cost);
			}
		}
		
		layOutGUI();
		refreshDomainFields();
	}

	/**
	 * Lays out the editor with the current orientation.
	 */
	protected void layOutGUI() {
		logger.fine("Laying out grid");
		
		this.removeAll();
				
		if (orientation == Orientation.PERSONS_ON_COLS) {
			
			this.setLayout( new MultiSizeGridLayout(sheet.getSharedCosts().size()+2, sheet.getSharedCosts().size()+2, 0, 0));
			
			// upper left corner
			this.add(new JPanel()); 
			
			// Column "headers"
			for (Person person : sheet.getPersons()) {
				PersonEditor pe = personEditors.get(person);
				pe.setVerticalAlignment(VAlignment.BOTTOM);
				this.add(pe);
			}
			
			// upper right corner
			this.add(new JPanel()); 
			
			// "rows"
			for (SharedCost sharedCost : sheet.getSharedCosts()) {
				SharedCostEditor sce = sharedCostEditors.get(sharedCost);
				sce.setVerticalAlignment(VAlignment.TOP);
				this.add(sce);
				
				for (Person person : sheet.getPersons()) {
					TransactionEditor pse = transactionEditors.get(new PersonSharedCostPair(person, sharedCost));
					pse.setVerticalAlignment(VAlignment.TOP);
					this.add(pse);
				}
				
				SharedCostBalancePanel scbp = sharedCostBalances.get(sharedCost);
				scbp.setVerticalAlignment(VAlignment.TOP);
				this.add(scbp);
			}
			
			// lower left corner
			this.add(new JPanel()); 
			
			for (Person person : sheet.getPersons()) {
				PersonBalancePanel pbp = personBalances.get(person);
				pbp.setVerticalAlignment(VAlignment.TOP);
				this.add(pbp);
			}
			
			// lower right corner
			this.add(new JPanel()); 
			
		} else {
			
			this.setLayout( new MultiSizeGridLayout(sheet.getPersons().size()+2, sheet.getSharedCosts().size()+2, 0, 0));
			
			// upper left corner
			this.add(new JPanel()); 
			
			// Column "headers"
			for (SharedCost sharedCost : sheet.getSharedCosts()) {
				SharedCostEditor sce = sharedCostEditors.get(sharedCost);
				sce.setVerticalAlignment(VAlignment.BOTTOM);
				this.add(sce);
			}
			
			// upper right corner
			this.add(new JPanel()); 
			
			// "rows"
			for (Person person : sheet.getPersons()) {
				PersonEditor pe = personEditors.get(person);
				pe.setVerticalAlignment(VAlignment.TOP);
				this.add(pe);
				
				for (SharedCost sharedCost : sheet.getSharedCosts()) {
					TransactionEditor pse = transactionEditors.get(new PersonSharedCostPair(person, sharedCost));
					pse.setVerticalAlignment(VAlignment.TOP);
					this.add(pse);
				}
				
				PersonBalancePanel pbp = personBalances.get(person);
				pbp.setVerticalAlignment(VAlignment.TOP);
				this.add(pbp);
			}
			
			// lower left corner
			this.add(new JPanel()); 
			
			for (SharedCost sharedCost : sheet.getSharedCosts()) {
				SharedCostBalancePanel scbp = sharedCostBalances.get(sharedCost);
				scbp.setVerticalAlignment(VAlignment.TOP);
				this.add(scbp);
			}
			
			// lower right corner
			this.add(new JPanel());
		}
		
		revalidate();
	}
		
	/**
	 * Re-orients (transposes) this balanceSheetGrid. 
	 * 
	 * @param orientation	the orientation
	 */
	public void setOrientation(Orientation orientation) {
		this.orientation = orientation != null ? orientation : Orientation.PERSONS_ON_COLS;
		layOutGUI();
	}

	public Orientation getOrientation() {
		return this.orientation;
	}

	@Override
	public void handleValueEdited(DomainEditorComponent<?> source) {
		logger.log(Level.FINER, "Caught event from {0}", source);
		
		if (source instanceof TransactionEditor) {
			
			TransactionEditor te = (TransactionEditor) source;
			SharedCost costInTransaction = te.getDomainComponent().getPayment().getSharedCost();
			
			// If cost is equally shared, must update all transaction editors for the cost
			// and balances of all persons taking part in the cost. 
			// Otherwise updating the balances of the shared cost and the person is enough.
			SharedCostEditor sce = (SharedCostEditor) sharedCostEditors.get(costInTransaction);
			if (sce.getSharingMethod() == SharingMethod.EQUAL) {
				shareEqually(costInTransaction);
			} else if (sce.getSharingMethod() == SharingMethod.PERCENTAGES) {
				// Refresh share values of 
				
			}
			
		} else if (source instanceof SharedCostEditor) {
			SharedCostEditor sce = (SharedCostEditor) source;
			SharedCost editedCost = sce.getDomainComponent();
			
			// Save sharing method
			currentData.setSharingMethod(editedCost, sce.getSharingMethod());
			currentData.setHasUnsavedChanges(true);
			
			// Apply new sharing method to all transaction editors for the edited shared cost
			for (Person person : sheet.getPersons()) {
				TransactionEditor teOfPerson = transactionEditors.get(new PersonSharedCostPair(person, editedCost));
				teOfPerson.setSharingMethod((SharingMethod)sce.getSharingMethod());
			}
			
			// If sharing method equal, apply it and refresh gui
			if (sce.getSharingMethod() == SharingMethod.EQUAL) {
				shareEqually(editedCost);
			}
			
		}  
		
		refreshDomainFields();
		fireDomainValueModified(source);
	}	
	

	
	/**
	 * Applies equal sharing of given cost and refreshes the GUI
	 * 
	 * @param sharedCost
	 */
	private void shareEqually(SharedCost sharedCost) {
		logger.log(Level.FINE, "Sharing equally cost {0}", sharedCost);
		
		// Collect participants
		Set<Person> participants = new HashSet<Person>();
		for (Person person : sheet.getPersons()) {
			TransactionEditor te = transactionEditors.get(new PersonSharedCostPair(person, sharedCost));
			if (te.participationButton.isSelected()) {
				participants.add(person);
			}
		}
		sharedCost.shareEqually(participants);
	}	
	
	@Override
	public Dimension getMaximumSize() {
		Dimension maximum = super.getMaximumSize();
        Dimension preferred = this.getPreferredSize();
        return new Dimension(maximum.width, preferred.height);
	}


	private void fireComponentSelected(SelectableComponent comp) {
		for (GridSelectionListener listener : this.gridListeners) {
			listener.componentSelected(comp);
		}
	}
	
	private void fireComponentDeselected(SelectableComponent comp) {
		for (GridSelectionListener listener : this.gridListeners) {
			listener.componentDeselected(comp);
		}
	}




	/**
	 * Adds a listener to be notified of events when the <code>SelectableComponent</code>
	 * selection changes in this balanceSheetGrid.
	 * 
	 * @param listener	the listener to add
	 * @return			<code>true</code> if the listener was added
	 */
	public boolean addBalanceSheetGridListener(GridSelectionListener listener) {
		return listener != this ? this.gridListeners.add(listener) : false;
	}

	/**
	 * Removes a listener from this balanceSheetGrid.
	 * 
	 * @param listener	the listener to remove
	 * @return			<code>true</code> if the listener was removed
	 */
	public boolean removeBalanceSheetGridListener(GridSelectionListener listener) {
		return this.gridListeners.remove(listener);
	}

	
	public void addPerson(Person person) {		
		logger.log(Level.FINE, "Adding person {0}", person);
		
		addEditorsFor(person);
		layOutGUI();
		refreshDomainFields();
		
		PersonEditor pe = personEditors.get(person);
		selectableComponentListener.setSelected(pe);
		pe.nameField.requestFocus();
	}

	/**
	 * @param person
	 */
	private void addEditorsFor(Person person) {
		PersonEditor pe = new PersonEditor(person);
		personEditors.put(person, pe);
		pe.addDomainEditorListener(this);
		pe.addMouseListener(selectableComponentListener);
		pe.addFocusListener(selectableComponentListener);
		setNormalBorder(pe);
		
		PersonBalancePanel pbp = new PersonBalancePanel(person);
		personBalances.put(person, pbp);
		pbp.addMouseListener(selectableComponentListener);
		pbp.addFocusListener(selectableComponentListener);
		setNormalBorder(pbp);
		
		for (SharedCost cost : sheet.getSharedCosts()) {
			TransactionEditor te = new TransactionEditor(person, cost, currentData.getSharingMethod(cost));
			transactionEditors.put(new PersonSharedCostPair(person, cost), te);
			te.addDomainEditorListener(this);
			te.addMouseListener(selectableComponentListener);
			te.addFocusListener(selectableComponentListener);
			setNormalBorder(te);
		}
	}
	
	public void removePerson(Person person) {	
		logger.log(Level.FINE, "Removing person {0}", person);
		
		removeEditorsFor(person);
		layOutGUI();
		refreshDomainFields();
	}

	/**
	 * @param person
	 */
	private void removeEditorsFor(Person person) {
		personEditors.remove(person);
		personBalances.remove(person);
		
		for (SharedCost cost : sheet.getSharedCosts()) {
			PersonSharedCostPair pair = new PersonSharedCostPair(person, cost);
			transactionEditors.remove(pair);
		}
		
		if (!personEditors.containsValue(selectedComponent) && !personBalances.containsValue(selectedComponent) && !transactionEditors.containsValue(selectedComponent)) {
			SelectableComponent removed = selectedComponent;
			selectedComponent = null;
			fireComponentDeselected(removed);
		}
	}
	
	public void addSharedCost(SharedCost sharedCost) {	
		logger.log(Level.FINE, "Adding cost {0}", sharedCost);
		
		addEditorsFor(sharedCost);
		layOutGUI();
		refreshDomainFields();
		
		SharedCostEditor sce = sharedCostEditors.get(sharedCost);
		selectableComponentListener.setSelected(sce);
		sce.nameField.requestFocus();
	}

	/**
	 * @param sharedCost
	 */
	private void addEditorsFor(SharedCost sharedCost) {
		SharedCostEditor sce = new SharedCostEditor(sharedCost, currentData.getSharingMethod(sharedCost));
		sharedCostEditors.put(sharedCost, sce);
		sce.addDomainEditorListener(this);
		sce.addMouseListener(selectableComponentListener);
		sce.addFocusListener(selectableComponentListener);
		setNormalBorder(sce);
		
		SharedCostBalancePanel scbp = new SharedCostBalancePanel(sharedCost);
		sharedCostBalances.put(sharedCost, scbp);
		scbp.addMouseListener(selectableComponentListener);
		scbp.addFocusListener(selectableComponentListener);
		setNormalBorder(scbp);
		
		for (Person person : sheet.getPersons()) {
			TransactionEditor te = new TransactionEditor(person, sharedCost, currentData.getSharingMethod(sharedCost));
			transactionEditors.put(new PersonSharedCostPair(person, sharedCost), te);
			te.addDomainEditorListener(this);
			te.addMouseListener(selectableComponentListener);
			te.addFocusListener(selectableComponentListener);
			setNormalBorder(te);
		}
	}
	
	public void removeSharedCost(SharedCost cost) {		
		logger.log(Level.FINE, "Removing cost {0}", cost);
		
		removeEditorsFor(cost);
		layOutGUI();
		refreshDomainFields();
	}

	/**
	 * @param cost
	 */
	private void removeEditorsFor(SharedCost cost) {
		for (Person person : sheet.getPersons()) {
			PersonSharedCostPair pair = new PersonSharedCostPair(person, cost);
			transactionEditors.remove(pair);
		}
		sharedCostEditors.remove(cost);
		sharedCostBalances.remove(cost);
		
		if (!sharedCostEditors.containsValue(selectedComponent) && !sharedCostBalances.containsValue(selectedComponent) && !transactionEditors.containsValue(selectedComponent)) {
			SelectableComponent removed = selectedComponent;
			selectedComponent = null;
			fireComponentDeselected(removed);
		}
	}


	protected static void setSelectedBorder(SelectableComponent comp) {
		comp.setBorder(BorderFactory.createLineBorder(Color.BLACK, 2));
	}
	
	protected static void setNormalBorder(SelectableComponent comp) {
		comp.setBorder(BorderFactory.createEtchedBorder());
	}	
	
	protected static void setPointedBorder(SelectableComponent comp) {
		comp.setBorder(BorderFactory.createLineBorder(Color.LIGHT_GRAY, 2));
	}

	@Override
	public BalanceSheet getDomainComponent() {
		return this.sheet;
	}
	
	@Override
	public void refreshDomainFields() {
		logger.fine("Refreshing domain fields");
		
		for (SharedCostEditor sce : sharedCostEditors.values()) {
			if (sce.getSharingMethod() == SharingMethod.EQUAL) {
				shareEqually(sce.getDomainComponent());
			}
		}
		
		Collection<DomainViewComponent<?>> comps = getViewComponents();
		for (DomainViewComponent<?> comp : comps) {
			comp.refreshDomainFields();
		}

	}

	/**
	 * @return
	 */
	private Collection<DomainViewComponent<?>> getViewComponents() {
		Set<DomainViewComponent<?>> comps = new LinkedHashSet<DomainViewComponent<?>>();
		comps.addAll(transactionEditors.values());
		comps.addAll(personEditors.values());
		comps.addAll(personBalances.values());
		comps.addAll(sharedCostEditors.values());
		comps.addAll(sharedCostBalances.values());
		return comps;
	}
	
	@Override
	public void setEnabled(boolean enabled) {
		super.setEnabled(enabled);
		for (DomainViewComponent<?> viewComp : getViewComponents()) {
			JComponent comp = (JComponent) viewComp;
			comp.setEnabled(enabled);
		}
	}
	
	/**
	 * Handles selection and deselection of editors and viewers when they are clicked
	 * or their inner components get focus. Highlights a component when mouse enters it. 
	 * 
	 * @author Anssi
	 *
	 */
	class SelectableComponentListener extends MouseAdapter implements FocusListener {
		
		@Override
		public void mouseClicked(MouseEvent e) {
			Component originator = e.getComponent();
			logger.log(Level.FINER, "Clicked component: {0}", originator);
			setSelected(originator);
		}

		/**
		 * @param comp
		 */
		protected void setSelected(Component comp) {
			if (comp instanceof SelectableComponent) {
				SelectableComponent selectable = (SelectableComponent) comp;
				if (selectable != selectedComponent) {
					// Deselect old, select new, notify domainEditorListeners
					if (selectedComponent != null) {
						selectedComponent.setSelected(false);
						setNormalBorder(selectedComponent);
						fireComponentDeselected(selectedComponent);
					}
					selectedComponent = selectable;
					selectedComponent.setSelected(true);
					setSelectedBorder(selectedComponent);
					fireComponentSelected(selectedComponent);
					
				} /*else {
					// Deselect old, notify domainEditorListeners
					selectedComponent.setSelected(false);
					this.selectedComponent = null;
					for (GridSelectionListener listener : this.gridListeners) {
						listener.componentDeselected(selectedComponent);
					}
				}*/ // doesn't work: deselects when an inner component such as text field is clicked on
				
				// TODO implement deselection when ctrl+clicking selected component
			}
			
		}
		
		@Override
		public void mouseEntered(MouseEvent e) {
			if (e.getComponent() instanceof SelectableComponent) {
				SelectableComponent sc = (SelectableComponent) e.getComponent();
				if (!sc.isSelected()) {
					setPointedBorder(sc);
				}
			}	
		}

		@Override
		public void mouseExited(MouseEvent e) {
			if (e.getComponent() instanceof SelectableComponent) {
				SelectableComponent sc = (SelectableComponent) e.getComponent();
				if (!sc.isSelected()) {
					setNormalBorder(sc);
				}
			}
		}
		
		@Override
		public void focusGained(FocusEvent e) {
			logger.log(Level.FINER, "Focus in component {0}", e.getComponent());
			setSelected(e.getComponent());
		}

		@Override
		public void focusLost(FocusEvent e) {
			logger.log(Level.FINER, "Focus removed from component {0}", e.getComponent());
			
		}
		
	}
}
